useWs Hook 的完整实现
useWs 是封装了 SharedWorker 通信逻辑的组合式函数,提供给 Vue 组件使用。它的职责是:创建 SharedWorker 实例、管理消息收发、将 SharedWorker 的事件转换为 Vue 的响应式数据。
// src/utils/use-ws.ts
import { ref, Ref } from 'vue'
import type { WebSocketClientOptions } from './websocket-client'
interface UseWsReturn {
init: () => void
send: (data: any) => void
close: () => void
message: Ref<any>
}
export function useWs(options: WebSocketClientOptions): UseWsReturn {
// 先保存回调函数(发送前要删除)
const hooks = ref({
onOpen: options.onOpen,
onError: options.onError,
onClose: options.onClose,
onMessage: options.onMessage,
})
// 创建 SharedWorker
const worker = new SharedWorker(
new URL('./shared-worker.js', import.meta.url),
{ name: 'ws-shared-worker' }
)
worker.port.start()
const message = ref(null)
// 接收 SharedWorker 消息
worker.port.onmessage = (event) => {
const { type, data } = event.data
if (type === 'open' && hooks.value.onOpen) {
hooks.value.onOpen(true)
} else if (type === 'error' && hooks.value.onError) {
hooks.value.onError({ message: data })
} else if (type === 'close' && hooks.value.onClose) {
hooks.value.onClose()
} else if (type === 'message') {
message.value = data
}
}
// 发送消息前移除不可序列化的属性
function postMessage(data: any) {
const id = options.url
// 删除函数属性(SharedWorker 不能传函数)
const cleanedOptions = { ...options }
const keysToRemove = ['onError', 'onClose', 'onOpen', 'onMessage', 'retryStrategy']
keysToRemove.forEach(key => delete cleanedOptions[key as keyof WebSocketClientOptions])
worker.port.postMessage({ ...data, id })
}
return {
init: () => {
const cleanedOptions = { ...options }
const keysToRemove = ['onError', 'onClose', 'onOpen', 'onMessage', 'retryStrategy']
keysToRemove.forEach(key => delete cleanedOptions[key as keyof WebSocketClientOptions])
postMessage({ type: 'init', data: cleanedOptions })
},
send: (data: any) => {
postMessage({ type: 'message', data })
},
close: () => {
postMessage({ type: 'close' })
},
message,
}
}
typescript
SharedWorker 的核心逻辑
SharedWorker 端负责管理 WebSocket 连接实例,处理来自各个 Tab 的消息:
// shared-worker.js
importScripts('./websocket-client.global.js')
const connections = {}
self.onconnect = function (event) {
const port = event.ports[0]
port.onmessage = function (event) {
const { type, data, id } = event.data
if (type === 'init') {
// 初始化 WebSocket 连接
const client = new WebSocketClient({
url: data.url,
onOpen: () => {
port.postMessage({ type: 'open' })
},
onError: () => {
port.postMessage({ type: 'error', data: 'WebSocket 连接异常' })
},
onClose: () => {
port.postMessage({ type: 'close' })
},
onMessage: (event, msgData) => {
port.postMessage({ type: 'message', data: msgData })
}
})
// 用 URL 作为 key 存储 client 和 port
if (!connections[id]) {
connections[id] = { client, ports: [port] }
} else {
connections[id].ports.push(port)
// 已有同 URL 的连接,不重复创建
}
}
if (type === 'message') {
const { client } = connections[id]
if (client) {
client.send(data)
}
}
if (type === 'close') {
const { client } = connections[id]
if (client) {
client.close()
delete connections[id]
}
}
}
port.start()
}
javascript
关键设计点:多 Port 消息广播
一个重要的问题是:当服务端发来消息时,SharedWorker 需要将消息广播给所有连接了同一个 WebSocket 实例的 Tab,而不是只发给最近一个。
// 收到消息时,广播给所有 port
onMessage: (event, msgData) => {
const connection = connections[id]
if (connection) {
connection.ports.forEach(p => {
p.postMessage({ type: 'message', data: msgData })
})
}
}
javascript
每个 Tab 在 init 时会将自己的 port 推入 connections[id].ports 数组中。收到消息后遍历所有 port 进行广播,确保每个 Tab 都能收到服务端的推送。
retryStrategy 的局限
retryStrategy 是一个函数,无法通过 postMessage 传递给 SharedWorker。解决方案有两种:
- 在 SharedWorker 内部写死默认的重连策略
- 通过配置项(如
retryCount、autoReconnect等)让 SharedWorker 自行构建策略
课程中标注了这个为 TODO 项,后续可以通过传递布尔值和数值参数来实现灵活的重连策略配置。
↑